package com.ease.util.logging {

   import flash.events.Event;
   import flash.utils.Dictionary;
   import flash.utils.getQualifiedClassName;

   import mx.logging.ILogger;
   import mx.logging.ILoggingTarget;
   import mx.logging.Log;
   import mx.logging.LogEvent;
   import mx.logging.LogEventLevel;

   public class LogManager {
      [ArrayElementType("mx.logging.LogEvent")]
      /**
       * Contains the log events dispatched while the log
       * config files are downloaded.
       * When a config is done downloading and its trace
       * targets are created, the pending log messages are re-logged.
       */
      private static var _pendingEvents:Array = [];

      /**
       * The number of pending config file downloads.
       */
      private static var _pendingLoadCount:int;

      /**
       * Custom level strings by level numeric values.
       */
      private static var _customLevelStringByLevel:Dictionary/*int, String*/ =
         new Dictionary();

      /**
       * The next custom level value available to use.
       */
      private static var _nextCustomLevel:int = LogEventLevel.FATAL + 1;


      /**
       * Returns a logger object for the given category.
       *
       * Note that if the class calling this method could be
       * instantiated multiple times, then it is recommended to
       * assign the result logger to a static variable because the
       * Flex <code>Log</code> class keeps a strong-reference cache
       * of all logger objects.
       *
       * @param category
       *    The <code>ILogger</code> category specified as Class,
       *    Object or String.
       *    Passing a Class or Object is a shortcut for passing
       *    the qualified class name.
       */
      public static function getLogger(category:*):ILogger {
         var categoryStr:String = category as String;
         if (categoryStr == null) {
            categoryStr = getQualifiedClassName(category);
            // : is an invalid character in category names.
            categoryStr = categoryStr.replace("::", ".");
         }

         var logger:ILogger = Log.getLogger(categoryStr);

         // If there is pending config downloading in progress
         // then listen to the log events so that message are
         // re-logged when the config logging targets are created.
         if (_pendingLoadCount != 0) {
            logger.addEventListener(LogEvent.LOG, onLog);
         }
         return logger;
      }

      /**
       * Adds the logging targets specified in the given xml.
       * The configuration xml schema is described in the class
       * documentation.
       */
      public static function addConfiguration(configXml:XML):void {
         var config:LogConfiguration = new LogConfiguration(configXml);
         for each (var target:ILoggingTarget in config.targets) {
            Log.addTarget(target);
         }
      }

      /**
       * Loads a log configuration from the given uri and adds its
       * logging targets.
       * The configuration xml schema is described in the class
       * documentation.
       *
       * @param uri
       *    The uri of the log configuration file.
       *
       * @param callback
       *    <code>function(error:Error, callContext:Object=null)</code>.
       *
       * @param callContext
       *    The context object passed to the addConfigurationByUri
       *     method.
       */
      public static function addConfigurationByUri(uri:String, callback:Function,
                                                   callContext:Object=null):void {
         var loader:ConfigurationLoader = new ConfigurationLoader();

         var pendingContext:Object = {pendingCallback: callback,
            pendingCallContext: callContext};

         ++_pendingLoadCount;
         loader.load(uri, onConfigLoaded, pendingContext);
      }

      /**
       * Adds a custom logging level.
       *
       * @param levelString
       *    The level string logged along with log messages.
       *
       * @return
       *    The level numeric value that can be used in <code>ILogger.log</code> calls.
       */
      public static function addCustomLevel(levelString:String):int {
         var level:int = getCustomLevel(levelString);
         if (level != -1) {
            return level;
         }
         level = _nextCustomLevel++;
         _customLevelStringByLevel[level] = levelString;
         return level;
      }

      /**
       * Removes a custom logging level.
       *
       * @param levelString
       *    The level string that was used in <code>addCustomLevel</code>.
       */
      public static function removeCustomLevel(levelString:String):void {
         var level:int = getCustomLevel(levelString);
         if (level == -1) {
            return;
         }
         delete _customLevelStringByLevel[level];
      }

      /**
       * Tests whether the given level is valid; i.e is a standard level or currently added
       * custom level.
       */
      public static function isValidLevel(level:int):Boolean {
         if (level <= LogEventLevel.FATAL) {
            return true;
         }
         return getCustomLevelString(level) != null;
      }

      /**
       * Returns the custom level string given a level numeric value, or null if no custom
       * level corresponds to the given value.
       */
      public static function getCustomLevelString(level:int):String {
         var levelStr:String = _customLevelStringByLevel[level]
         return levelStr;
      }

      /**
       * Returns the custom logging level corresponding to the given level string, or
       * -1 if none.
       */
      private static function getCustomLevel(levelStr:String):int {
         for (var levelObj:Object in _customLevelStringByLevel) {
            var level:int = int(levelObj);
            if (_customLevelStringByLevel[level] == levelStr) {
               return level;
            }
         }
         return -1;
      }

      /**
       * The callback of the <code>ConfigurationLoader.load</code> function.
       */
      private static function onConfigLoaded(configXml:XML, error:Error,
                                             callContext:Object):void {
         if (error == null) {
            addConfiguration(configXml);
         }
         if (--_pendingLoadCount == 0) {
            flushPendingEvents();
         }
         callContext.pendingCallback(error, callContext.pendingCallContext);
      }

      /**
       * The event handler of the og events disptached while
       * config files are being downloaded.
       */
      private static function onLog(event:LogEvent):void {
         _pendingEvents.push(event);
      }

      /**
       * Re-logs the messages corresponding to the pending
       * events.
       */
      private static function flushPendingEvents():void {
         for each (var event:LogEvent in _pendingEvents) {
            var logger:ILogger = ILogger(event.target);
            // This could be called multiple times if there are
            // multiple events for a logger, but that's ok.
            logger.removeEventListener(LogEvent.LOG, onLog);

            logger.log(event.level, event.message);
         }
         _pendingEvents = [];
      }
   }
}

////////////////////////////////////////////////////////////////////////
// Private classes
////////////////////////////////////////////////////////////////////////

import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.net.URLRequest;
import flash.net.URLLoader;

/**
 * Loads a logging configuration given its uri.
 */
class ConfigurationLoader {
   private var _callback:Function;
   private var _callContext:Object;

   /**
    * Loads a log configuration xml from the given uri.
    *
    * @param uri
    *    The uri of the log configuration file.
    *
    * @param callback
    *    <code>function(configXml:XML, error:Error,
    *       callContext:Object=null)</code>.
    *
    * @param callContext
    *    The context object passed to the load method.
    */
   public function load(uri:String, callback:Function,
                        callContext:Object=null):void {
      _callback = callback;
      _callContext = callContext;

      var loader:URLLoader = new URLLoader();
      loader.addEventListener(Event.COMPLETE, onLoadSuccess);
      loader.addEventListener(IOErrorEvent.IO_ERROR, onLoadError);
      loader.load(new URLRequest(uri));
   }

   private function onLoadSuccess(event:Event):void {
      var loader:URLLoader = URLLoader(event.target);
      var configXml:XML = new XML(loader.data);
      _callback(configXml, null, _callContext);
   }

   private function onLoadError(event:IOErrorEvent):void {
      _callback(null, new Error(event.text), _callContext);
   }
}